package com.yanyeori.framework.jira.jiraclient;

import net.sf.json.JSONArray;
import net.sf.json.JSONNull;
import net.sf.json.JSONObject;

import java.sql.Timestamp;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Utility functions for translating between JSON and fields.
 */
public final class Field {

    /**
     * Field metadata structure.
     */
    public static final class Meta {
        public boolean required;
        public String type;
        public String items;
        public String name;
        public String system;
        public String custom;
        public int customId;
    }

    /**
     * Field update operation.
     */
    public static final class Operation {
        public String name;
        public Object value;

        /**
         * Initialises a new update operation.
         *
         * @param name  Operation name
         * @param value Field value
         */
        public Operation(String name, Object value) {
            this.name = name;
            this.value = value;
        }
    }

    /**
     * Allowed value types.
     */
    public enum ValueType {
        KEY("key"), NAME("name"), ID_NUMBER("id"), VALUE("value");

        private String typeName;

        ValueType(String typeName) {
            this.typeName = typeName;
        }

        @Override
        public String toString() {
            return typeName;
        }
    }

    /**
     * Value and value type pair.
     */
    public static final class ValueTuple {
        public final String type;
        public final Object value;

        /**
         * Initialises the value tuple.
         *
         * @param type
         * @param value
         */
        public ValueTuple(String type, Object value) {
            this.type = type;
            this.value = (value != null ? value : JSONNull.getInstance());
        }

        /**
         * Initialises the value tuple.
         *
         * @param type
         * @param value
         */
        public ValueTuple(ValueType type, Object value) {
            this(type.toString(), value);
        }
    }

    public static final String ASSIGNEE = "assignee";
    public static final String ATTACHMENT = "attachment";
    public static final String CHANGE_LOG = "changelog";
    public static final String CHANGE_LOG_ENTRIES = "histories";
    public static final String CHANGE_LOG_ITEMS = "items";
    public static final String COMMENT = "comment";
    public static final String COMPONENTS = "components";
    public static final String DESCRIPTION = "description";
    public static final String DUE_DATE = "duedate";
    public static final String FIX_VERSIONS = "fixVersions";
    public static final String ISSUE_LINKS = "issuelinks";
    public static final String ISSUE_TYPE = "issuetype";
    public static final String LABELS = "labels";
    public static final String PARENT = "parent";
    public static final String PRIORITY = "priority";
    public static final String PROJECT = "project";
    public static final String REPORTER = "reporter";
    public static final String RESOLUTION = "resolution";
    public static final String RESOLUTION_DATE = "resolutiondate";
    public static final String STATUS = "status";
    public static final String SUBTASKS = "subtasks";
    public static final String SUMMARY = "summary";
    public static final String TIME_TRACKING = "timetracking";
    public static final String VERSIONS = "versions";
    public static final String VOTES = "votes";
    public static final String WATCHES = "watches";
    public static final String WORKLOG = "worklog";
    public static final String TIME_ESTIMATE = "timeestimate";
    public static final String TIME_SPENT = "timespent";
    public static final String CREATED_DATE = "created";
    public static final String UPDATED_DATE = "updated";
    public static final String TRANSITION_TO_STATUS = "to";
    public static final String SECURITY = "security";

    public static final String DATE_FORMAT = "yyyy-MM-dd";
    public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";

    private Field() {
    }

    /**
     * Gets a boolean value from the given object.
     *
     * @param b a Boolean instance
     * @return a boolean primitive or false if b isn't a Boolean instance
     */
    public static boolean getBoolean(Object b) {
        boolean result = false;

        if (b instanceof Boolean)
            result = (Boolean) b;

        return result;
    }

    /**
     * Gets a list of comments from the given object.
     *
     * @param c          a JSONObject instance
     * @param restclient REST client instance
     * @param issueKey   key of the parent issue
     * @return a list of comments found in c
     */
    public static List<Comment> getComments(Object c, RestClient restclient, String issueKey) {
        List<Comment> results = new ArrayList<>();

        if (c instanceof JSONObject && !((JSONObject) c).isNullObject()) {
            results = getResourceArray(
                    Comment.class,
                    ((Map) c).get("comments"),
                    restclient,
                    issueKey
            );
        }

        return results;
    }

    /**
     * Gets a list of work logs from the given object.
     *
     * @param c          a JSONObject instance
     * @param restclient REST client instance
     * @return a list of work logs found in c
     */
    public static List<WorkLog> getWorkLogs(Object c, RestClient restclient) {
        List<WorkLog> results = new ArrayList<>();

        if (c instanceof JSONObject && !((JSONObject) c).isNullObject())
            results = getResourceArray(WorkLog.class, ((Map) c).get("worklogs"), restclient);

        return results;
    }

    /**
     * Gets a list of remote links from the given object.
     *
     * @param c          a JSONObject instance
     * @param restclient REST client instance
     * @return a list of remote links found in c
     */
    public static List<RemoteLink> getRemoteLinks(Object c, RestClient restclient) {
        List<RemoteLink> results = new ArrayList<>();

        if (c instanceof JSONArray)
            results = getResourceArray(RemoteLink.class, c, restclient);

        return results;
    }

    /**
     * Gets a date from the given object.
     *
     * @param d a string representation of a date
     * @return a Date instance or null if d isn't a string
     */
    public static Date getDate(Object d) {
        Date result = null;

        if (d instanceof String) {
            SimpleDateFormat df = new SimpleDateFormat(DATE_FORMAT);
            result = df.parse((String) d, new ParsePosition(0));
        } else if (d instanceof Number) {
            result = new Date((long) d);
        }

        return result;
    }

    /**
     * Gets a date with a time from the given object.
     *
     * @param d a string representation of a date
     * @return a Date instance or null if d isn't a string
     */
    public static Date getDateTime(Object d) {
        Date result = null;

        if (d instanceof String) {
            SimpleDateFormat df = new SimpleDateFormat(DATETIME_FORMAT);
            result = df.parse((String) d, new ParsePosition(0));
        } else if (d instanceof Number) {
            result = new Date((long) d);
        }

        return result;
    }

    /**
     * Gets an floating-point number from the given object.
     *
     * @param i an Double instance
     * @return an floating-point number or null if i isn't a Double instance
     */
    public static Double getDouble(Object i) {
        Double result = null;

        if (i instanceof Double)
            result = (Double) i;

        return result;
    }

    /**
     * Gets an integer from the given object.
     *
     * @param i an Integer instance
     * @return an integer primitive or 0 if i isn't an Integer instance
     */
    public static int getInteger(Object i) {
        int result = 0;

        if (i instanceof Integer)
            result = (Integer) i;

        return result;
    }

    /**
     * +     * Gets a long from the given object.
     * +     *
     * +     * @param i a Long or an Integer instance
     * +     *
     * +     * @return a long primitive or 0 if i isn't a Long or an Integer instance
     * +
     */
    public static long getLong(Object i) {
        long result = 0;
        if (i instanceof Long) {
            result = (Long) i;
        } else if (i instanceof Integer) {
            result = (Integer) i;
        }
        return result;
    }

    /**
     * Gets a generic map from the given object.
     *
     * @param keytype Map key data type
     * @param valtype Map value data type
     * @param m       a JSONObject instance
     * @return a Map instance with all entries found in m
     */
    public static <TK, TV> Map<TK, TV> getMap(Class<TK> keytype, Class<TV> valtype, Object m) {
        Map<TK, TV> result = new HashMap<>();

        if (m instanceof JSONObject && !((JSONObject) m).isNullObject()) {
            for (Object k : ((Map) m).keySet()) {
                Object v = ((Map) m).get(k);

                if (k.getClass() == keytype && v.getClass() == valtype)
                    result.put((TK) k, (TV) v);
            }
        }

        return result;
    }

    /**
     * Gets a JIRA resource from the given object.
     *
     * @param type       Resource data type
     * @param r          a JSONObject instance
     * @param restclient REST client instance
     * @return a Resource instance or null if r isn't a JSONObject instance
     */
    public static <T extends Resource> T getResource(Class<T> type, Object r, RestClient restclient) {
        return getResource(type, r, restclient, null);
    }

    /**
     * Gets a JIRA resource from the given object.
     *
     * @param type       Resource data type
     * @param r          a JSONObject instance
     * @param restclient REST client instance
     * @param parentId   id/key of the parent resource
     * @return a Resource instance or null if r isn't a JSONObject instance
     */
    public static <T extends Resource> T getResource(Class<T> type, Object r, RestClient restclient, String parentId) {
        T result = null;

        if (r instanceof JSONObject && !((JSONObject) r).isNullObject()) {
            if (type == Attachment.class)
                result = (T) new Attachment(restclient, (JSONObject) r);
            else if (type == ChangeLog.class)
                result = (T) new ChangeLog(restclient, (JSONObject) r);
            else if (type == ChangeLogEntry.class)
                result = (T) new ChangeLogEntry(restclient, (JSONObject) r);
            else if (type == ChangeLogItem.class)
                result = (T) new ChangeLogItem(restclient, (JSONObject) r);
            else if (type == Comment.class)
                result = (T) new Comment(restclient, (JSONObject) r, parentId);
            else if (type == Component.class)
                result = (T) new Component(restclient, (JSONObject) r);
            else if (type == CustomFieldOption.class)
                result = (T) new CustomFieldOption(restclient, (JSONObject) r);
            else if (type == Issue.class)
                result = (T) new Issue(restclient, (JSONObject) r);
            else if (type == IssueLink.class)
                result = (T) new IssueLink(restclient, (JSONObject) r);
            else if (type == IssueType.class)
                result = (T) new IssueType(restclient, (JSONObject) r);
            else if (type == LinkType.class)
                result = (T) new LinkType(restclient, (JSONObject) r);
            else if (type == Priority.class)
                result = (T) new Priority(restclient, (JSONObject) r);
            else if (type == Project.class)
                result = (T) new Project(restclient, (JSONObject) r);
            else if (type == ProjectCategory.class)
                result = (T) new ProjectCategory(restclient, (JSONObject) r);
            else if (type == RemoteLink.class)
                result = (T) new RemoteLink(restclient, (JSONObject) r);
            else if (type == Resolution.class)
                result = (T) new Resolution(restclient, (JSONObject) r);
            else if (type == Status.class)
                result = (T) new Status(restclient, (JSONObject) r);
            else if (type == Transition.class)
                result = (T) new Transition(restclient, (JSONObject) r);
            else if (type == User.class)
                result = (T) new User(restclient, (JSONObject) r);
            else if (type == Visibility.class)
                result = (T) new Visibility(restclient, (JSONObject) r);
            else if (type == Version.class)
                result = (T) new Version(restclient, (JSONObject) r);
            else if (type == Votes.class)
                result = (T) new Votes(restclient, (JSONObject) r);
            else if (type == Watches.class)
                result = (T) new Watches(restclient, (JSONObject) r);
            else if (type == WorkLog.class)
                result = (T) new WorkLog(restclient, (JSONObject) r);
            else if (type == Security.class)
                result = (T) new Security(restclient, (JSONObject) r);
        }

        return result;
    }

    /**
     * Gets a string from the given object.
     *
     * @param value a String instance
     * @return a String or null if s isn't a String instance
     */
    public static String getString(Object value) {
        if (value == null) return null;
        if (value instanceof String || value instanceof Number) return value.toString();
        return null;
    }

    public static String getString(Object value, String objectProperty) {
        if (value == null || value instanceof JSONNull) {
            return null;
        } else if (value instanceof JSONObject) {
            return getString(((JSONObject) value).get(objectProperty), "value");
        } else if (value instanceof JSONArray) {
            JSONArray array = (JSONArray) value;
            if (array.isEmpty()) return null;
            return getString(array.get(0), objectProperty);
        }
        return value.toString();
    }

    /**
     * Gets a list of strings from the given object.
     *
     * @param value a JSONArray instance
     * @return a list of strings found in sa
     */
    public static List<String> getStringArray(Object value) {
        return getStringArray(value, "value");
    }

    public static List<String> getStringArray(Object value, String objectProperty) {
        List<String> results = new ArrayList<>();
        if (value instanceof JSONArray) {
            for (Object s : (JSONArray) value) {
                results.add(getString(s, objectProperty));
            }
        }
        return results;
    }

    /**
     * Gets a list of JIRA resources from the given object.
     *
     * @param type       Resource data type
     * @param ra         a JSONArray instance
     * @param restclient REST client instance
     * @return a list of Resources found in ra
     */
    public static <T extends Resource> List<T> getResourceArray(Class<T> type, Object ra, RestClient restclient) {
        return getResourceArray(type, ra, restclient, null);
    }

    /**
     * Gets a list of JIRA resources from the given object.
     *
     * @param type       Resource data type
     * @param ra         a JSONArray instance
     * @param restclient REST client instance
     * @param parentId   id/key of the parent resource
     * @return a list of Resources found in ra
     */
    public static <T extends Resource> List<T> getResourceArray(Class<T> type, Object ra, RestClient restclient, String parentId) {
        List<T> results = new ArrayList<>();

        if (ra instanceof JSONArray) {
            for (Object v : (JSONArray) ra) {
                T item;

                if (parentId != null) {
                    item = getResource(type, v, restclient, parentId);
                } else {
                    item = getResource(type, v, restclient);
                }

                if (item != null)
                    results.add(item);
            }
        }

        return results;
    }

    /**
     * Gets a time tracking object from the given object.
     *
     * @param tt a JSONObject instance
     * @return a TimeTracking instance or null if tt isn't a JSONObject instance
     */
    public static TimeTracking getTimeTracking(Object tt) {
        TimeTracking result = null;

        if (tt instanceof JSONObject && !((JSONObject) tt).isNullObject())
            result = new TimeTracking((JSONObject) tt);

        return result;
    }

    /**
     * Extracts field metadata from an editmeta JSON object.
     *
     * @param name     Field name
     * @param editmeta Edit metadata JSON object
     * @return a Meta instance with field metadata
     * @throws JiraException when the field is missing or metadata is bad
     */
    public static Meta getFieldMetadata(String name, JSONObject editmeta) throws JiraException {

        if (editmeta.isNullObject() || !editmeta.containsKey(name))
            throw new JiraException("Field '" + name + "' does not exist or read-only");

        Map f = (Map) editmeta.get(name);
        Meta m = new Meta();

        m.required = Field.getBoolean(f.get("required"));
        m.name = Field.getString(f.get("name"));

        if (!f.containsKey("schema"))
            throw new JiraException("Field '" + name + "' is missing schema metadata");

        Map schema = (Map) f.get("schema");

        m.type = Field.getString(schema.get("type"));
        m.items = Field.getString(schema.get("items"));
        m.system = Field.getString(schema.get("system"));
        m.custom = Field.getString(schema.get("custom"));
        m.customId = Field.getInteger(schema.get("customId"));

        return m;
    }

    /**
     * Converts the given value to a date.
     *
     * @param value New field value
     * @return a Date instance or null
     */
    public static Date toDate(Object value) {
        if (value instanceof Date || value == null)
            return (Date) value;

        String dateStr = value.toString();
        SimpleDateFormat df = new SimpleDateFormat(DATE_FORMAT);
        if (dateStr.length() > DATE_FORMAT.length()) {
            df = new SimpleDateFormat(DATETIME_FORMAT);
        }
        return df.parse(dateStr, new ParsePosition(0));
    }

    /**
     * Converts an iterable type to a JSON array.
     *
     * @param iter   Iterable type containing field values
     * @param type   Name of the item type
     * @param custom Name of the custom type
     * @return a JSON-encoded array of items
     */
    public static JSONArray toArray(Iterable iter, String type, String custom) throws JiraException {
        JSONArray results = new JSONArray();

        if (type == null)
            throw new JiraException("Array field metadata is missing item type");

        for (Object val : iter) {
            Operation oper = null;
            Object realValue = null;
            Object realResult = null;

            if (val instanceof Operation) {
                oper = (Operation) val;
                realValue = oper.value;
            } else
                realValue = val;

            if (type.equals("component") || type.equals("group") ||
                    type.equals("user") || type.equals("version")) {

                JSONObject itemMap = new JSONObject();

                if (realValue instanceof ValueTuple) {
                    ValueTuple tuple = (ValueTuple) realValue;
                    itemMap.put(tuple.type, tuple.value.toString());
                } else
                    itemMap.put(ValueType.NAME.toString(), realValue.toString());

                realResult = itemMap;
            } else if (type.equals("option") ||
                    (type.equals("string") && custom != null
                            && (custom.equals("com.atlassian.jira.plugin.system.customfieldtypes:multicheckboxes") ||
                            custom.equals("com.atlassian.jira.plugin.system.customfieldtypes:multiselect")))) {

                realResult = new JSONObject();
                ((JSONObject) realResult).put(ValueType.VALUE.toString(), realValue.toString());
            } else if (type.equals("string"))
                realResult = realValue.toString();

            if (oper != null) {
                JSONObject operMap = new JSONObject();
                operMap.put(oper.name, realResult);
                results.add(operMap);
            } else
                results.add(realResult);
        }

        return results;
    }

    /**
     * Converts the given value to a JSON object.
     *
     * @param name     Field name
     * @param value    New field value
     * @param editmeta Edit metadata JSON object
     * @return a JSON-encoded field value
     * @throws JiraException                 when a value is bad or field has invalid metadata
     * @throws UnsupportedOperationException when a field type isn't supported
     */
    public static Object toJson(String name, Object value, JSONObject editmeta) throws JiraException, UnsupportedOperationException {
        Meta m = getFieldMetadata(name, editmeta);
        if (m.type == null) {
            throw new JiraException("Field '" + name + "' is missing metadata type");
        }

        switch (m.type) {
            case "array":
                if (value == null)
                    value = new ArrayList<>(0);
                else if (!(value instanceof Iterable))
                    value = Arrays.asList(value);

                return toArray((Iterable) value, m.items, m.custom);
            case "date": {
                if (value == null)
                    return JSONNull.getInstance();

                Date d = toDate(value);
                if (d == null)
                    throw new JiraException("Field '" + name + "' expects a date value or format is invalid");

                SimpleDateFormat df = new SimpleDateFormat(DATE_FORMAT);
                return df.format(d);
            }
            case "datetime": {
                if (value == null)
                    return JSONNull.getInstance();
                else if (!(value instanceof Timestamp))
                    throw new JiraException("Field '" + name + "' expects a Timestamp value");

                SimpleDateFormat df = new SimpleDateFormat(DATETIME_FORMAT);
                return df.format(value);
            }
            case "issuetype":
            case "priority":
            case "user":
            case "resolution":
            case "securitylevel": {
                JSONObject json = new JSONObject();

                if (value == null)
                    return JSONNull.getInstance();
                else if (value instanceof ValueTuple) {
                    ValueTuple tuple = (ValueTuple) value;
                    json.put(tuple.type, tuple.value.toString());
                } else
                    json.put(ValueType.NAME.toString(), value.toString());

                return json.toString();
            }
            case "project":
            case "issuelink": {
                JSONObject json = new JSONObject();

                if (value == null)
                    return JSONNull.getInstance();
                else if (value instanceof ValueTuple) {
                    ValueTuple tuple = (ValueTuple) value;
                    json.put(tuple.type, tuple.value.toString());
                } else
                    json.put(ValueType.KEY.toString(), value.toString());

                return json.toString();
            }
            case "string":
                if (value == null)
                    return "";
                else if (value instanceof List)
                    return toJsonMap((List) value);
                else if (value instanceof ValueTuple) {
                    JSONObject json = new JSONObject();
                    ValueTuple tuple = (ValueTuple) value;
                    json.put(tuple.type, tuple.value.toString());
                    return json.toString();
                }
                return value.toString();
            case "option":
                if (value == null)
                    return "";
                else if (value instanceof List)
                    return toJsonArray((List) value);
                else if (value instanceof ValueTuple) {
                    JSONObject json = new JSONObject();
                    ValueTuple tuple = (ValueTuple) value;
                    json.put(tuple.type, tuple.value.toString());
                    return json.toString();
                }
                return value.toString();
            case "timetracking":
                if (value == null)
                    return JSONNull.getInstance();
                else if (value instanceof TimeTracking)
                    return ((TimeTracking) value).toJsonObject();
                break;
            case "number":
                if (value == null) //Non mandatory number fields can be set to null
                    return JSONNull.getInstance();
                else if (!(value instanceof Integer) && !(value instanceof Double) && !(value
                        instanceof Float) && !(value instanceof Long)) {
                    throw new JiraException("Field '" + name + "' expects a Numeric value");
                }
                return value;
            case "any":
                if (value == null)
                    return JSONNull.getInstance();
                else if (value instanceof List)
                    return toJsonArray((List) value);
                else if (value instanceof ValueTuple) {
                    JSONObject json = new JSONObject();
                    ValueTuple tuple = (ValueTuple) value;
                    json.put(tuple.type, tuple.value.toString());
                    return json.toString();
                } else if (value instanceof TimeTracking)
                    return ((TimeTracking) value).toJsonObject();

                return value;
        }

        throw new UnsupportedOperationException(m.type + " is not a supported field type");
    }

    /**
     * Converts the given map to a JSON object.
     *
     * @param list List of values to be converted
     * @return a JSON-encoded map
     */
    public static Object toJsonMap(List list) {
        JSONObject json = new JSONObject();

        for (Object item : list) {
            if (item instanceof ValueTuple) {
                ValueTuple vt = (ValueTuple) item;
                json.put(vt.type, vt.value.toString());
            } else
                json.put(ValueType.VALUE.toString(), item.toString());
        }

        return json.toString();
    }

    public static Object toJsonArray(List list) {
        JSONArray array = new JSONArray();
        JSONObject json;

        for (Object item : list) {
            json = new JSONObject();

            if (item instanceof ValueTuple) {
                ValueTuple vt = (ValueTuple)item;
                json.put(vt.type, vt.value.toString());
            } else {
                json.put(ValueType.VALUE.toString(), item.toString());
            }
            array.add(json);
        }

        return array.toString();
    }

    /**
     * Create a value tuple with value type of key.
     *
     * @param key The key value
     * @return a value tuple
     */
    public static ValueTuple valueByKey(String key) {
        return new ValueTuple(ValueType.KEY, key);
    }

    /**
     * Create a value tuple with value type of name.
     *
     * @param name The name value
     * @return a value tuple
     */
    public static ValueTuple valueByName(String name) {
        return new ValueTuple(ValueType.NAME, name);
    }

    /**
     * Create a value tuple with value type of ID number.
     *
     * @param id The ID number value
     * @return a value tuple
     */
    public static ValueTuple valueById(String id) {
        return new ValueTuple(ValueType.ID_NUMBER, id);
    }

    /**
     * Create a value tuple with value type of value.
     *
     * @param value The value
     * @return a value tuple
     */
    public static ValueTuple valueByValue(String value) {
        return new ValueTuple(ValueType.VALUE, value);
    }
}
